/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.tencent.tws.assistant.widget;

import java.util.Calendar;
import java.util.Locale;

import android.annotation.Widget;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView;

import com.tencent.tws.assistant.widget.NumberPicker.Formatter;
import com.tencent.tws.assistant.widget.NumberPicker.OnValueChangeListener;
import com.tencent.tws.sharelib.R;

@Widget
public class TimePicker extends FrameLayout {

	private static final String LOG_TAG = TimePicker.class.getSimpleName();

	private static final boolean DEFAULT_ENABLED_STATE = true;

	private static final int HOURS_IN_HALF_DAY = 12;

	/**
	 * A no-op callback used in the constructor to avoid null checks later in
	 * the code.
	 */
	private static final OnTimeChangedListener NO_OP_CHANGE_LISTENER = new OnTimeChangedListener() {
		public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
		}
	};

	// state
	private boolean mIs24HourView;

	private boolean mIsAm;

	// ui components
	private final NumberPicker mHourSpinner;

	private final NumberPicker mMinuteSpinner;

	private final NumberPicker mAmPmSpinner;

	private final EditText mHourSpinnerInput;

	private final EditText mMinuteSpinnerInput;

	private final EditText mAmPmSpinnerInput;

	private final ImageView mImageViewDivider;
	// private final TextView mDivider;

	// Note that the legacy implementation of the TimePicker is
	// using a button for toggling between AM/PM while the new
	// version uses a NumberPicker spinner. Therefore the code
	// accommodates these two cases to be backwards compatible.
	private final Button mAmPmButton;

	private final String[] mAmPmStrings;

	private boolean mIsEnabled = DEFAULT_ENABLED_STATE;

	// callbacks
	private OnTimeChangedListener mOnTimeChangedListener;

	private Calendar mTempCalendar;

	private Locale mCurrentLocale;

	private boolean mHourWithTwoDigit;
	private char mHourFormat;
	private String mHourName;
	private String mMinuteName;

	// default is false
	private boolean mUnitShown = false;
	
	/**
	 * The callback interface used to indicate the time has been adjusted.
	 */
	public interface OnTimeChangedListener {

		/**
		 * @param view
		 *            The view associated with this listener.
		 * @param hourOfDay
		 *            The current hour.
		 * @param minute
		 *            The current minute.
		 */
		void onTimeChanged(TimePicker view, int hourOfDay, int minute);
	}

	public TimePicker(Context context) {
		this(context, null);
	}

	public TimePicker(Context context, AttributeSet attrs) {
		this(context, attrs, R.attr.timePickerStyle);
	}

	public TimePicker(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);

		// initialization based on locale
		setCurrentLocale(Locale.getDefault());
		mHourName = getResources().getString(R.string.calendar_hour);
		mMinuteName = getResources().getString(R.string.calendar_mintue);

		// process style attributes
		TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.TimePicker, defStyle, 0);
		int layoutResourceId = attributesArray.getResourceId(R.styleable.TimePicker_layout, R.layout.time_picker);
		attributesArray.recycle();

		LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
		inflater.inflate(layoutResourceId, this, true);

		// hour
		mHourSpinner = (NumberPicker) findViewById(R.id.hour);
		mHourSpinner.setTextAlignType(NumberPicker.ALIGN_RIGHT_TYPE);
		mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
			public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
				updateInputState();
				if (!is24HourView()) {
					if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY)
							|| (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) {
						mIsAm = !mIsAm;
						updateAmPmControl();
					}
				}
				onTimeChanged();
			}
		});
		mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input);
		mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);

		// minute
		mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
		mMinuteSpinner.setTextAlignType(NumberPicker.ALIGN_LEFT_TYPE);
		mMinuteSpinner.setMinValue(0);
		mMinuteSpinner.setMaxValue(59);
		mMinuteSpinner.setOnLongPressUpdateInterval(100);
		if (mUnitShown) {
			mMinuteSpinner.setFormatter(mMinuteFormatter);
		} else {
			mMinuteSpinner.setFormatter(mNoUnitMinuteFormatter);
		}
		mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
			public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
				updateInputState();
				onTimeChanged();
			}
		});
		mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input);
		mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);

		// tws-start using custom ampm::2014-8-22
		/* Get the localized am/pm strings and use them in the spinner */
		mAmPmStrings = getResources().getStringArray(R.array.tws_calendar_ampm);
		// tws-end using custom ampm::2014-8-22

		mImageViewDivider = (ImageView) findViewById(R.id.timepicker_divider);

		// am/pm
		View amPmView = findViewById(R.id.amPm);
		if (amPmView instanceof Button) {
			mAmPmSpinner = null;
			mAmPmSpinnerInput = null;
			mAmPmButton = (Button) amPmView;
			mAmPmButton.setOnClickListener(new OnClickListener() {
				public void onClick(View button) {
					button.requestFocus();
					mIsAm = !mIsAm;
					updateAmPmControl();
					onTimeChanged();
				}
			});
		} else {
			mAmPmButton = null;
			mAmPmSpinner = (NumberPicker) amPmView;
			mAmPmSpinner.setMinValue(0);
			mAmPmSpinner.setMaxValue(1);
			mAmPmSpinner.setDisplayedValues(mAmPmStrings);
			mAmPmSpinner.setOnValueChangedListener(new OnValueChangeListener() {
				public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
					updateInputState();
					picker.requestFocus();
					mIsAm = !mIsAm;
					updateAmPmControl();
					onTimeChanged();
				}
			});
			mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input);
			mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
		}

		if (android.os.Build.VERSION.SDK_INT > 18) {
			getHourFormatData();
		}
		// update controls to initial state
		updateHourControl();
		updateMinuteControl();
		updateAmPmControl();

		setOnTimeChangedListener(NO_OP_CHANGE_LISTENER);

		// set to current time
		setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY));
		setCurrentMinute(mTempCalendar.get(Calendar.MINUTE));

		if (!isEnabled()) {
			setEnabled(false);
		}

		// set the content descriptions
		setContentDescriptions();
	}
	
	public void setUnitShown(boolean unitShown) {
		if (mUnitShown == unitShown)
			return;

		mUnitShown = unitShown;
		if (mUnitShown) {
			mMinuteSpinner.setFormatter(mMinuteFormatter);
			mHourSpinner.setFormatter(mHourFormatter);
		} else {
			mMinuteSpinner.setFormatter(mNoUnitMinuteFormatter);
			mHourSpinner.setFormatter(mNoUnitHourFormatter);
		}

		mMinuteSpinner.invalidate();
		mHourSpinner.invalidate();
	}

	private void getHourFormatData() {
		final Locale defaultLocale = Locale.getDefault();
		final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, (mIs24HourView) ? "Hm"
				: "hm");
		final int lengthPattern = bestDateTimePattern.length();
		mHourWithTwoDigit = false;
		char hourFormat = '\0';
		// Check if the returned pattern is single or double 'H', 'h', 'K', 'k'.
		// We also save
		// the hour format that we found.
		for (int i = 0; i < lengthPattern; i++) {
			final char c = bestDateTimePattern.charAt(i);
			if (c == 'H' || c == 'h' || c == 'K' || c == 'k') {
				mHourFormat = c;
				if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) {
					mHourWithTwoDigit = true;
				}
				break;
			}
		}
	}

	@Override
	public void setEnabled(boolean enabled) {
		if (mIsEnabled == enabled) {
			return;
		}
		super.setEnabled(enabled);
		mMinuteSpinner.setEnabled(enabled);
		/*
		 * if (mDivider != null) { mDivider.setEnabled(enabled); }
		 */
		mHourSpinner.setEnabled(enabled);
		if (mAmPmSpinner != null) {
			mAmPmSpinner.setEnabled(enabled);
		} else {
			mAmPmButton.setEnabled(enabled);
		}
		mIsEnabled = enabled;
	}

	@Override
	public boolean isEnabled() {
		return mIsEnabled;
	}

	@Override
	protected void onConfigurationChanged(Configuration newConfig) {
		super.onConfigurationChanged(newConfig);
		setCurrentLocale(newConfig.locale);
	}

	/**
	 * Sets the current locale.
	 * 
	 * @param locale
	 *            The current locale.
	 */
	private void setCurrentLocale(Locale locale) {
		if (locale.equals(mCurrentLocale)) {
			return;
		}
		mCurrentLocale = locale;
		mTempCalendar = Calendar.getInstance(locale);
	}

	/**
	 * Used to save / restore state of time picker
	 */
	private static class SavedState extends BaseSavedState {

		private final int mHour;

		private final int mMinute;

		private SavedState(Parcelable superState, int hour, int minute) {
			super(superState);
			mHour = hour;
			mMinute = minute;
		}

		private SavedState(Parcel in) {
			super(in);
			mHour = in.readInt();
			mMinute = in.readInt();
		}

		public int getHour() {
			return mHour;
		}

		public int getMinute() {
			return mMinute;
		}

		@Override
		public void writeToParcel(Parcel dest, int flags) {
			super.writeToParcel(dest, flags);
			dest.writeInt(mHour);
			dest.writeInt(mMinute);
		}

		@SuppressWarnings({ "unused", "hiding" })
		public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
			public SavedState createFromParcel(Parcel in) {
				return new SavedState(in);
			}

			public SavedState[] newArray(int size) {
				return new SavedState[size];
			}
		};
	}

	@Override
	protected Parcelable onSaveInstanceState() {
		Parcelable superState = super.onSaveInstanceState();
		return new SavedState(superState, getCurrentHour(), getCurrentMinute());
	}

	@Override
	protected void onRestoreInstanceState(Parcelable state) {
		SavedState ss = (SavedState) state;
		super.onRestoreInstanceState(ss.getSuperState());
		setCurrentHour(ss.getHour());
		setCurrentMinute(ss.getMinute());
	}

	/**
	 * Set the callback that indicates the time has been adjusted by the user.
	 * 
	 * @param onTimeChangedListener
	 *            the callback, should not be null.
	 */
	public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) {
		mOnTimeChangedListener = onTimeChangedListener;
	}

	/**
	 * @return The current hour in the range (0-23).
	 */
	public Integer getCurrentHour() {
		int currentHour = mHourSpinner.getValue();
		if (is24HourView()) {
			return currentHour;
		} else if (mIsAm) {
			return currentHour % HOURS_IN_HALF_DAY;
		} else {
			return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
		}
	}

	/**
	 * Set the current hour.
	 */
	public void setCurrentHour(Integer currentHour) {
		setCurrentHour(currentHour, true);
	}

	private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) {
		// why was Integer used in the first place?
		if (currentHour == null || currentHour == getCurrentHour()) {
			return;
		}
		if (!is24HourView()) {
			// convert [0,23] ordinal to wall clock display
			if (currentHour >= HOURS_IN_HALF_DAY) {
				mIsAm = false;
				if (currentHour > HOURS_IN_HALF_DAY) {
					currentHour = currentHour - HOURS_IN_HALF_DAY;
				}
			} else {
				mIsAm = true;
				if (currentHour == 0) {
					currentHour = HOURS_IN_HALF_DAY;
				}
			}
			updateAmPmControl();
		}
		mHourSpinner.setValue(currentHour);
		if (notifyTimeChanged) {
			onTimeChanged();
		}
	}

	/**
	 * Set whether in 24 hour or AM/PM mode.
	 * 
	 * @param is24HourView
	 *            True = 24 hour mode. False = AM/PM.
	 */
	public void setIs24HourView(Boolean is24HourView) {
		if (mIs24HourView == is24HourView) {
			return;
		}
		// cache the current hour since spinner range changes and BEFORE
		// changing mIs24HourView!!
		int currentHour = getCurrentHour();
		// Order is important here.
		mIs24HourView = is24HourView;
		if (android.os.Build.VERSION.SDK_INT > 18) {
			getHourFormatData();
		}
		updateHourControl();
		// set value after spinner range is updated - be aware that because
		// mIs24HourView has
		// changed then getCurrentHour() is not equal to the currentHour we
		// cached before so
		// explicitly ask for *not* propagating any onTimeChanged()
		setCurrentHour(currentHour, false /* no onTimeChanged() */);
		updateMinuteControl();
		updateAmPmControl();
	}

	/**
	 * @return true if this is in 24 hour view else false.
	 */
	public boolean is24HourView() {
		return mIs24HourView;
	}

	/**
	 * @return The current minute.
	 */
	public Integer getCurrentMinute() {
		return mMinuteSpinner.getValue();
	}

	/**
	 * Set the current minute (0-59).
	 */
	public void setCurrentMinute(Integer currentMinute) {
		if (currentMinute == getCurrentMinute()) {
			return;
		}
		mMinuteSpinner.setValue(currentMinute);
		onTimeChanged();
	}

	@Override
	public int getBaseline() {
		return mHourSpinner.getBaseline();
	}

	@Override
	public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
		onPopulateAccessibilityEvent(event);
		return true;
	}

	@Override
	public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
		super.onPopulateAccessibilityEvent(event);

		int flags = DateUtils.FORMAT_SHOW_TIME;
		if (mIs24HourView) {
			flags |= DateUtils.FORMAT_24HOUR;
		} else {
			flags |= DateUtils.FORMAT_12HOUR;
		}
		mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour());
		mTempCalendar.set(Calendar.MINUTE, getCurrentMinute());
		String selectedDateUtterance = DateUtils.formatDateTime(mContext, mTempCalendar.getTimeInMillis(), flags);
		event.getText().add(selectedDateUtterance);
	}

	private void updateHourControl() {
		if (is24HourView()) {
			// 'k' means 1-24 hour
			if (mHourFormat == 'k') {
				mHourSpinner.setMinValue(1);
				mHourSpinner.setMaxValue(24);
			} else {
				mHourSpinner.setMinValue(0);
				mHourSpinner.setMaxValue(23);
			}
		} else {
			// 'K' means 0-11 hour
			if (mHourFormat == 'K') {
				mHourSpinner.setMinValue(0);
				mHourSpinner.setMaxValue(11);
			} else {
				mHourSpinner.setMinValue(1);
				mHourSpinner.setMaxValue(12);
			}
		}
		
		if (mUnitShown) {
			mHourSpinner.setFormatter(mHourFormatter);
		} else {
			mHourSpinner.setFormatter(mNoUnitHourFormatter);
		}
	}

	private void updateMinuteControl() {
		if (is24HourView()) {
			mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
		} else {
			mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
		}
	}

	private void updateAmPmControl() {
		if (is24HourView()) {
			if (mAmPmSpinner != null) {
				// mImageViewDivider.setVisibility(View.GONE);
				mAmPmSpinner.setVisibility(View.GONE);
			} else {
				// mImageViewDivider.setVisibility(View.GONE);
				mAmPmButton.setVisibility(View.GONE);
			}
		} else {
			int index = mIsAm ? Calendar.AM : Calendar.PM;
			if (mAmPmSpinner != null) {
				mAmPmSpinner.setValue(index);
				// mImageViewDivider.setVisibility(View.VISIBLE);
				mAmPmSpinner.setVisibility(View.VISIBLE);
			} else {
				mAmPmButton.setText(mAmPmStrings[index]);
				// mImageViewDivider.setVisibility(View.VISIBLE);
				mAmPmButton.setVisibility(View.VISIBLE);
			}
		}
		sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
	}

	private void onTimeChanged() {
		sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
		if (mOnTimeChangedListener != null) {
			mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute());
		}
	}

	private void setContentDescriptions() {
		// Minute
		trySetContentDescription(mMinuteSpinner, R.id.increment, R.string.time_picker_increment_minute_button);
		trySetContentDescription(mMinuteSpinner, R.id.decrement, R.string.time_picker_decrement_minute_button);
		// Hour
		trySetContentDescription(mHourSpinner, R.id.increment, R.string.time_picker_increment_hour_button);
		trySetContentDescription(mHourSpinner, R.id.decrement, R.string.time_picker_decrement_hour_button);
		// AM/PM
		if (mAmPmSpinner != null) {
			trySetContentDescription(mAmPmSpinner, R.id.increment, R.string.time_picker_increment_set_pm_button);
			trySetContentDescription(mAmPmSpinner, R.id.decrement, R.string.time_picker_decrement_set_am_button);
		}
	}

	private void trySetContentDescription(View root, int viewId, int contDescResId) {
		View target = root.findViewById(viewId);
		if (target != null) {
			target.setContentDescription(mContext.getString(contDescResId));
		}
	}

	private void updateInputState() {
		// Make sure that if the user changes the value and the IME is active
		// for one of the inputs if this widget, the IME is closed. If the user
		// changed the value via the IME and there is a next input the IME will
		// be shown, otherwise the user chose another means of changing the
		// value and having the IME up makes no sense.
		InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
		if (inputMethodManager != null) {
			if (inputMethodManager.isActive(mHourSpinnerInput)) {
				mHourSpinnerInput.clearFocus();
				inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
			} else if (inputMethodManager.isActive(mMinuteSpinnerInput)) {
				mMinuteSpinnerInput.clearFocus();
				inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
			} else if (inputMethodManager.isActive(mAmPmSpinnerInput)) {
				mAmPmSpinnerInput.clearFocus();
				inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
			}
		}
	}

	Formatter mHourFormatter = new NumberPicker.Formatter() {
		@Override
		public String format(int value) {
			return value + mHourName;
		}
	};
	Formatter mNoUnitHourFormatter = new NumberPicker.Formatter() {
		@Override
		public String format(int value) {
			if (value < 10) {
				return "0" + value;
			}

			return value + "";
		}
	};

	Formatter mMinuteFormatter = new NumberPicker.Formatter() {
		@Override
		public String format(int value) {
			return value + mMinuteName;
		}
	};
	Formatter mNoUnitMinuteFormatter = new NumberPicker.Formatter() {
		@Override
		public String format(int value) {
			if (value < 10) {
				return "0" + value;
			}

			return value + "";
		}
	};
}
